/* CssUtils.js
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

$package("dowry.util");

/**
 * Helper class to perform CSS-related operations, such as
 * adding / removing / getting / etc.  This implementation
 * uses its own internal CSS library, which is relatively
 * efficient for Dowry-related activities... but it
 * supports subclassing if you would rather use the CSS
 * utilities in your favorite Javascript library.
 *
 * <p>
 * CssUtils allows you to prefix all Dowry-related CSS
 * information with <code>dowry:</code>, to provide a clean
 * namespace for Dowry-related CSS class names.  This is
 * turned off by default, but can be enabled by setting the
 * <code>usePrefix</code> property to true.
 * </p>
 *
 */
$class("CssUtils").$as(
{
    /**
     * Constant for the prefix to place before CSS class
     * names, when <code>usePrefix</code> is set to true.
     *
     */
    CSS_PREFIX : "dowry:",

    /**
     * A property to flag whether CSS class names should use
     * a special "dowry:" prefix to avoid namespace
     * collisions.  The default is false.
     *
     */
    usePrefix : false,

    /**
     * Associates the given className with the given
     * element.
     *
     *
     * @param ele        the element
     *
     * @param className  the CSS class name
     *
     *
     */
    addClassName : function(ele, className)
    {
        if (!this.hasClassName(ele, className))
        {
            ele = $(ele);
            className = this._toClassName(className);

            if (ele != null &&
                className != null)
            {
                var arr = this._toArray(ele);
                arr[arr.length] = className;
                ele.className = arr.join(" ");
            }
        }
    },

    /**
     * Gets all the CSS class names for the specified
     * element.
     *
     *
     * @param ele        the element
     *
     * @return           array of CSS class names
     *                   associated with this element
     *
     */
    getClassNames : function(ele)
    {
        var classNames = new Array();
        ele = $(ele);

        if (ele != null)
        {
            var rawClassNames = this._toArray(ele);

            // convert raw class names, based on
            // prefixing
            if (rawClassNames != null)
            {
                for (var i=0; i < rawClassNames.length; i++)
                {
                    var className = this._fromClassName(rawClassNames[i]);
                    if (className != null)
                    {
                        classNames[classNames.length] = className;
                    }
                }
            }
        }

        return classNames;
    },

    /**
     * Finds all elements that match the specified CSS
     * class name.  This implementation is based on <a
     * href="http://www.robertnyman.com/2005/11/07/the-ultimate-getelementsbyclassname/">
     * Robert Nyman's "Ultimate getElementsByClassName()".
     * </a>
     *
     * @param className  the CSS class name (or array of
     *                   class names)
     *
     * @param root       (optional) the root element to
     *                   search under, if not specified
     *                   then the entire document will be
     *                   searched
     *
     * @return           an array of elements matching the
     *                   specified class name
     *
     * @author           Jonathan Snook
     *                   <http://www.snook.ca/jonathan>
     *
     * @author           Robert Nyman
     *                   <http://www.robertnyman.com>
     *
    */
    getElementsByClassName : function(className, root)
    {
        var eles = new Array();
        root = $(root);

        // convert our class names to use prefixing...
        if (typeof className == "string")
        {
            className = this._toClassName(className);
        }
        else if (className != null &&
                 className.join != null)
        {
            var classNameArr = new Array();
            for (var i=0; i < className.length; i++)
            {
                var s = this._toClassName(className[i]);
                if (s != null)
                {
                    classNameArr[classNameArr.length] = s;
                }
            }

            className = classNameArr;
        }

        // if no root is specified, then assume its the
        // document
        if (root == null)
        {
            root = document;
        }

        // make sure we have a class after we are
        // done converting the class names... we use
        // our own method instead of prototype / dojo
        // because it seems an order or magnitude more
        // performant
        if (className != null)
        {
            eles = this._getElementsByClassName(root, "*", className);
        }

        return eles;
    },

    /**
     * Indicates whether the given element contains the
     * specified class name.
     *
     * @param ele        the element
     *
     * @param className  the CSS class name
     *
     * @return           true, if the element contains the
     *                   specified CSS class name; false
     *                   otherwise
     *
     */
    hasClassName : function(ele, className)
    {
        var rc = false;

        ele = $(ele);
        className = this._toClassName(className);

        if (ele != null &&
            className != null)
        {
            var arr = this._toArray(ele);
            for (var i=0; i < arr.length; i++)
            {
                if (arr[i] == className)
                {
                    rc = true;
                    break;
                }
            }
        }

        return rc;
    },

    /**
     * Removes the association between the given className
     * and the given element.
     *
     * @param ele        the element
     *
     * @param className  the CSS class name
     *
     *
     */
    removeClassName : function(ele, className)
    {
        if (this.hasClassName(ele, className))
        {
            ele = $(ele);
            className = this._toClassName(className);

            if (ele != null &&
                className != null)
            {
                var arr2 = new Array();
                var arr = this._toArray(ele);
                for (var i=0; i < arr.length; i++)
                {
                    if (arr[i] != className)
                    {
                        arr2[arr2.length] = arr[i];
                    }
                }

                if (arr2.length > 0)
                {
                    ele.className = arr2.join(" ");
                }
                else
                {
                    ele.className = "";
                }
            }
        }
    },

    /**
     * Remove a prefix from the given className if indicated
     * and returns the modified className.  If a prefix is
     * expected but not found, null is returned.
     *
     * @param className  properly prefixed className
     *
     * @return           the CSS class name (with no prefix)
     *
     */
    _fromClassName : function(className)
    {
        if (typeof className == "string")
        {
            // remove the css prefix, if the usePrefix
            // flag is true
            if (this.usePrefix)
            {
                if (className.indexOf(this.CSS_PREFIX) == 0)
                {
                    className = className.substring(this.CSS_PREFIX.length,
                                                    className.length);
                }
                else
                {
                    className = null;
                }
            }
        }
        else
        {
            className = null;
        }

        return className;
    },

    /**
     * Gets the element's raw CSS class names as an array.
     *
     * @param ele  the element
     *
     * @return     an array of string CSS class name
     *
     */
    _toArray : function(ele)
    {
        var arr = null;

        if (ele != null &&
            ele.className != null)
        {
            arr = ele.className.split(/\s+/);
            if (arr.length == 1 && arr[0] == "")
            {
                arr = null;
            }
        }

        if (arr == null)
        {
            arr = new Array();
        }

        return arr;
    },

    /**
     * Adds a prefix to the given className if indicated and
     * returns the modified className.
     *
     * @param className  the CSS class name
     *
     * @return           properly prefixed className
     *
     */
    _toClassName : function(className)
    {
        if (typeof className == "string")
        {
            // treat an empty string as if there is
            // no class name
            if (className == "")
            {
                className = null;
            }

            // add the css prefix, if the usePrefix
            // flag is true
            else if (this.usePrefix)
            {
                className = this.CSS_PREFIX + className;
            }
        }
        else
        {
            className = null;
        }

        return className;
    },


    /**
     * Implementation of getElementsByClassName provided by
     * Robert Nyman's "Ultimate getElementsByClassName()",
     * with one small change... the rule for dealing with
     * arrays of class names changes from "all must match"
     * to "any must match".
     *
     * @author             Jonathan Snook
     *                     <http://www.snook.ca/jonathan>
     *
     * @author             Robert Nyman
     *                     <http://www.robertnyman.com>
     *
     * @param oElm         the root element
     *
     * @param strTagName   the string tag name
     *
     * @param oClassNames  the CSS class name (or array of
     *                     class names to search for
     *
     * @return             array of elements matching any
     *                     of the specified class names
     *
     */
    _getElementsByClassName : function(oElm, strTagName, oClassNames)
    {
        var arrReturnElements = new Array();

        // determine the elements to search...
        var arrElements = (strTagName == "*" && oElm.all)?
            oElm.all : oElm.getElementsByTagName(strTagName);

        // always treat class names as an array
        if (typeof oClassNames == "string")
        {
            oClassNames = [ oClassNames ];
        }

        // generate list of regexes
        var arrRegExpClassNames = new Array();
        for(var i=0; i<oClassNames.length; i++)
        {
            arrRegExpClassNames[i] =
               new RegExp("(^|\\s)" +
                          oClassNames[i].replace(/\-/g, "\\-") +
                          "(\\s|$)");
        }

        // look for matches
        var oElement;
        for(var j=0; j<arrElements.length; j++)
        {
            oElement = arrElements[j];
            for(var k=0; k<arrRegExpClassNames.length; k++)
            {
                if(arrRegExpClassNames[k].test(oElement.className))
                {
                    oElement.dowryMatchingClassName = oClassNames[k];

                    arrReturnElements[arrReturnElements.length] = oElement;
                    break;
                }
            }
        }

        return arrReturnElements;
    }
});